package cn.cosmosx.base.id;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import java.text.DecimalFormat;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;

/**
 * ID生成规则
 */
@Slf4j
@Getter
enum IDRule {
    /**
     * 全局流水号标识（1位“G”）+系统ID（2位）+时间戳（14位）+序号（7位）24位长度
     */
    GLOBAL_SERIAL("全局流水号", "GQS", new DecimalFormat("0000000"), 9999999),

    /**
     * 资源请求ID："REQUEST_ID" + 17位时间戳 + 6位自增序列号
     */
    REQUEST_ID("资源请求ID", "REQUEST", new DecimalFormat("000000"), 999999, Unit.MILLIS_SECONDS),

    /**
     * 资源请求ID："MI" + 23位时间戳 + 2位自增序列号
     */
    MINI_ID("短ID", "MI", new DecimalFormat("000"), 999),
    ;
    /**
     * 锁
     */
    private final ReentrantLock lock;
    /**
     * 描述
     */
    private final String name;
    /**
     * 前缀
     */
    private final String prefix;
    /**
     * 后缀
     */
    private final AtomicInteger suffix;
    /**
     * 后缀格式化
     */
    private final DecimalFormat suffixFormat;
    /**
     * 后缀最大值
     */
    private final int suffixMax;
    /**
     * 时间戳格式化精确单位
     */
    private final Unit unit;
    /**
     * 上一个时间轮（单位：ms）
     */
    private LocalDateTime lastTimeWheel;
    /**
     * 步长，默认为单机，步长为1。（服务器数量与步长保持一致。如2台服务器，步长为2）
     */
    private int step = 1;

    IDRule(String name, String prefix, DecimalFormat suffixFormat, int suffixMax) {
        // 默认时间戳到秒级别，共14位
        this(name, prefix, suffixFormat, suffixMax, Unit.SECONDS);
    }

    IDRule(String name, String prefix, DecimalFormat suffixFormat, int suffixMax, Unit unit) {
        this(new ReentrantLock(), name, prefix, new AtomicInteger(0), suffixFormat, suffixMax, unit);
    }

    IDRule(ReentrantLock lock, String name, String prefix, AtomicInteger suffix, DecimalFormat suffixFormat, int suffixMax, Unit unit) {
        this.lock = lock;
        this.name = name;
        this.prefix = prefix;
        this.suffix = suffix;
        this.suffixFormat = suffixFormat;
        this.suffixMax = suffixMax;
        this.unit = unit;
    }

    /**
     * 取当前服务器时间戳（ms）
     */
    LocalDateTime ts() {
        return LocalDateTime.now();
    }

    /**
     * 生成最终生成的序列号
     */
    public final String generate() {
        lock.lock();
        try {
            int suffix = suffix();
            String ts = unit.getFormat().format(handleTs(suffix));
            return prefix + ts + suffixFormat.format(suffix);
        } finally {
            lock.unlock();
        }
    }

    /**
     * 处理时间戳
     *
     * @param suffix - 后缀
     * @return - newTs
     */
    private LocalDateTime handleTs(int suffix) {
        // 初始化时间戳
        LocalDateTime ts = initTs(suffix);
        // 时钟回拨处理
        if (isClockCallback(ts)) {
            ts = ts();
        }
        // 重置时间轮
        setLastTimeWheel(ts);
        return ts;
    }

    /**
     * 初始化时间戳
     *
     * @param suffix - 自增序列号
     */
    private LocalDateTime initTs(int suffix) {
        LocalDateTime ts = ts();
        if (getLastTimeWheel() == null) {
            setLastTimeWheel(ts);
            return ts;
        }
        // 时间戳不变，后缀序列号归零处理，延迟1个单位时间
        if (suffix == step && unit.getFormat().format(getLastTimeWheel()).equals(unit.getFormat().format(ts))) {
            log.warn("{} auto-increment-serial is reset:{}.", this.name(), suffix);
            sleepNanoSeconds(getUnit());
            ts = ts();
        }
        return ts;
    }

    /**
     * 时钟是否回拨
     * 承受时钟回拨的误差范围最大为500ms
     *
     * @param newTs - 当前服务器时间戳
     */
    private boolean isClockCallback(LocalDateTime newTs) {
        final LocalDateTime last = getLastTimeWheel();
        if (newTs.isAfter(last) || newTs.isEqual(last)) {
            return false;
        }
        long diffMs = Duration.between(last, newTs).toMillis();
        if (diffMs - 500L <= 0) {
            log.warn("{} clock time is callback, differ:{}ms.", this.name(), suffix);
            try {
                TimeUnit.MILLISECONDS.sleep(diffMs);
            } catch (InterruptedException ignored) {
            }
            return true;
        }
        throw new RuntimeException("Current server clock is callback, differ:" + diffMs + "ms.");
    }

    /**
     * 生成自增序列号
     */
    private int suffix() {
        int s = suffix.addAndGet(step);
        if (s > suffixMax) {
            // 重置自增序列号
            suffix.set(0);
            s = suffix.addAndGet(step);
        }
        return s;
    }

    private void sleepNanoSeconds(Unit unit) {
        // 睡眠纳秒数
        long nanosSeconds;
        switch (unit) {
            case SECONDS:
                nanosSeconds = (long) Math.pow(10, 9);
                break;
            case MILLIS_SECONDS:
                nanosSeconds = (long) Math.pow(10, 6);
                break;
            case MICROS_SECONDS:
                nanosSeconds = (long) Math.pow(10, 3);
                break;
            default:
                nanosSeconds = 1L;
                break;
        }
        try {
            // 睡眠 range ns
            TimeUnit.NANOSECONDS.sleep(nanosSeconds);
        } catch (InterruptedException ignored) {
        }
    }

    void setLastTimeWheel(LocalDateTime millisSeconds) {
        this.lastTimeWheel = millisSeconds;
    }

    /**
     * 设置自增序列号步长，服务器数量与步长保持一致。如2台服务器，步长为2。以此类推
     *
     * @param step - 步长
     */
    public void setStep(int step) {
        this.step = step;
    }

    /**
     * 时间戳格式化精确单位
     */
    @Getter
    @AllArgsConstructor
    enum Unit {
        YEARS(DateTimeFormatter.ofPattern("yyyy")),
        MONTHS(DateTimeFormatter.ofPattern("yyyyMM")),
        DAYS(DateTimeFormatter.ofPattern("yyyyMMdd")),
        SECONDS(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")),
        MILLIS_SECONDS(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")),
        MICROS_SECONDS(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSSSSS")),
        NANOS_SECONDS(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSSSSSSSS"));
        private final DateTimeFormatter format;
    }
}
